package ngat.oss.simulation;

import java.util.*;

import ngat.util.*;
import ngat.phase2.*;
import ngat.util.logging.*;
import ngat.astrometry.*;

/** Calculates how much time is left for a group to execute in a given window.*/
public class BasicTimingConstraintWindowCalculator implements TimingConstraintWindowCalculator {

    public static final long SAMP_INT = 10*60*1000L;

    public static final long FGB = 600000;

    public static final long DAYMS = 24*3600*1000L;

    /** An exec model.*/
    ExecutionTimingModel execModel;

    /** The site.*/
    Site site;

    /** Time resolution (ms).*/
    long dt;

    /** Logging.*/
    LogProxy logger;

    /** Create a BasicTimingConstraintWindowCalculator using the supplied execModel and
     * time resolution..
     */
    public BasicTimingConstraintWindowCalculator(ExecutionTimingModel execModel, Site site, long dt) {
	this.execModel = execModel;
	this.site = site;
	this.dt = dt;

	Logger slogger = LogManager.getLogger("SIM");
	logger = new LogProxy("BTCWC","",slogger);

    }

    /** Returns the window which includes t or null.*/
    public TimeWindow getWindow(Group group, ExecutionStatistics history, long time) {

        logger.method("getWindow(G,h,t)").log(2, "Checking feasible window for: Group: "+group.getName()+
					      ", class="+group.getClass().getName()+
					      ", including T="+ScheduleSimulator.sdf.format(new Date(time)));
	
        long grpExpDate = group.getExpiryDate();
	
        // Special case: Expired before start of search interval.
        if (grpExpDate <= time) {
            logger.log(2, "Group "+group.getName()+" failed: POST_EXPIRY");
            return null;
        }

        if (group instanceof MonitorGroup) {

            MonitorGroup mg = (MonitorGroup)group;
            long   startDate     = mg.getStartDate();
            long   period        = mg.getPeriod();
            float  floatFraction = mg.getFloatFraction();

            // Special case: Not started monitoring.
            if (startDate > time) {
                logger.log(2, "Group "+group.getName()+" failed: MG_PRE_START");
                return null;
            }

            // Special case: Monitoring completed.
            long endDate = mg.getEndDate();
            if (endDate <= time) {
                logger.log(2, "Group "+group.getName()+" failed: MG_POST_END");
                return null;
            }

            // first period before ts.
            double fPeriod = (double)( time - startDate ) / (double)period;
            int i = (int)Math.rint(fPeriod);

	    // calculate the window
	    long startFloat = startDate +
		(long) (((double)i - (double)floatFraction / 2.0)*(double)period);

	    long endFloat   = startDate +
		(long)(((double)i + (double)floatFraction / 2.0)*(double)period);

	    if ((startFloat <= time) && (endFloat >= time)) {
		// Check it wasnt done already inside the window
		//if (history.lastExecuted > 0 && 
		if (history.lastExecuted < startFloat ) {
		    return new TimeWindow(startFloat, endFloat);
		} else {
		    logger.log(2, "Group "+group.getName()+" failed: MG_LAST DONE: "+
			       ScheduleSimulator.sdf.format(new Date(history.lastExecuted)));
		}
	    } else {
		logger.log(2, "Group "+group.getName()+" failed NOT IN FLOAT: "+
			   ScheduleSimulator.sdf.format(new Date(startFloat))+", "+
			   ScheduleSimulator.sdf.format(new Date(endFloat)));	
	    }
	    
        } else if
            (group instanceof FixedGroup) {

            // the only possible window is (fg-b, fg+b)
	    
            FixedGroup fg = (FixedGroup)group;
	    
            // Special case: Already done before ts
            if (history.lastExecuted > 0 && history.lastExecuted < time) {
                logger.log(2, "Group "+group.getName()+" failed: FG_DONE");
                return null;
            }

            // FG can be done within specified minutes of the fixed time.
            long fixed = fg.getFixedTime();
            //      if (  ((fixed - fixedGroupPreStartBufferTime) > te) ||
            //            ((fixed + fixedGroupPostStartLapseTime) < ts) ) {
            //          logger.log(3, "Group "+group.getName()+" failed: FG_OUTSIDE_SLOP");
            //          return null;
            //      }
            // TEMP Use fixed FGB time
            if (  ((fixed - FGB) > time) ||
                  ((fixed + FGB) < time) ) {
                logger.log(2, "Group "+group.getName()+" failed: FG_OUTSIDE_SLOP");
                return null;
            }

            return new TimeWindow(fixed - FGB, fixed + FGB);
	    
	} else if
	    (group instanceof RepeatableGroup) {
	    
	    // depends on last exec from hist. Cannot predict future beyond one window
	    RepeatableGroup rg = (RepeatableGroup)group;
	    
	    long startDate   = rg.getStartDate();
	    long endDate     = rg.getEndDate();
	    long minInterval = rg.getMinimumInterval();
	    int  maxRepeats  = rg.getMaximumRepeats();
	    
	    // Special case: Not started monitoring.
	    if (startDate > time) {
		logger.log(2, "Group "+group.getName()+" failed: RG_PRE_START");
		return null;
	    }
	    
	    // Special case: Monitoring completed.
	    if (endDate < time) {
		logger.log(2, "Group "+group.getName()+" failed: RG_PAST_END");
		return null;
	    }
	    
	    // Special case: Too soon after last exec
	    if((time - history.lastExecuted) < minInterval) {
		logger.log(2, "Group "+group.getName()+" failed: REPEATABLE_TOO_SOON_AFTER_LAST_EXEC");
		return null;
	    }

	    // Special case: Repeat count exceeded
	    if (maxRepeats < history.countExecutions) {
		logger.log(2, "Group "+group.getName()+" failed: REPEATABLE_TOO_MANY_REPEATS");
		return null;
	    }
	    
	    // lets restrict the end time to now plus minInterval
	    return new TimeWindow(history.lastExecuted + minInterval, time + minInterval);
	    	    
	} else if
	    (group instanceof EphemerisGroup) {
	    
	    // similar to mg but using phase - only if not already done by start
	    EphemerisGroup eg = (EphemerisGroup)group;
	    
	    long   startDate     = eg.getStart();
	    long   endDate       = eg.getEnd();
	    long   period        = eg.getPeriod();
	    double phase         = (double)eg.getPhase();
	    double slopFraction  = (double)eg.getSlopPhase();
	    
	    
	    // Special case: Already done before ts
	    if (history.lastExecuted > 0 && history.lastExecuted < time) {
		logger.log(2, "Group "+group.getName()+" failed: EG_DONE");
		return null;
	    }
	    
	    // Special case: Not started monitoring.
	    if (startDate > time) {
		logger.log(2, "Group "+group.getName()+" failed: EG_PRE_START");
		return null;
	    }
	    // Special case: Monitoring completed.
	    if (endDate <= time) {
		logger.log(2, "Group "+group.getName()+" failed: EG_POST_END");
		return null;
	    }
	    
	    // first period before ts.
	    double fPeriod = Math.floor((time - startDate)/period);
	    int i = (int)fPeriod;
	    
	    // calculate the window
	    long startWindow = startDate + (long)((i + phase - slopFraction/2.0) * period);
	    long endWindow   = startDate + (long)((i + phase + slopFraction/2.0) * period);

	    if ( ((startWindow <= time) && (endWindow >= time)))
		return new TimeWindow(startWindow, endWindow);
	
	} else {
	    
	    // (fx.start, fx.end) but only if not already done by start.
	    
	    long startFlex = group.getStartingDate();
	    long endFlex = grpExpDate;
	    
	    // Special case: Already done before ts
	    if (history.lastExecuted > 0 && history.lastExecuted < time) {
		logger.log(2, "Group "+group.getName()+" failed: FLEXG_DONE");
		return null;
	    }

	    if ((startFlex < time) &&
		(endFlex > time))
		return new TimeWindow(startFlex, endFlex);
	}

	return null;

    }


    /** Returns a List of feasible windows for the specified group which overlap the interval
     * [ts,te]. The windows may start any time before te and end anytime after ts.
     * @param group The group for which feasible windows are required.
     * @param history The execution history of the group.
     * @param ts Start of the overlap interval.
     * @param te End of the overlap interval.
     */
    public List listFeasibleWindows(Group group,
				    ExecutionStatistics history,
				    long ts, 
				    long te) {

	// Find any windows which start before te and end after ts and have NOT already been executed.
	// TODO Need exec history  also ?
	logger.method("listFeasibleWindows(G,h,ts,te)").log(2, "Checking feasible windows for: Group: "+group.getName()+
						   "between TS="+ScheduleSimulator.sdf.format(new Date(ts))+
						   "and TE="+ScheduleSimulator.sdf.format(new Date(te)));
	
	List list = new Vector();

	long grpExpDate = group.getExpiryDate();

	// Special case: Expired before start of search interval.
	if (grpExpDate <= ts) {		
	    logger.log(2, "Group "+group.getName()+" failed: POST_EXPIRY");
	    return null;
	}

	if (group instanceof MonitorGroup) {

	    // work out all monitor periods from start to end of expiry/endmon

	    // gi = [s + i*p - 0.5*w, s + i*p + 0.5*w] for i = 0 to ibig
	    // ibig = (e-s)/p + 1

	    MonitorGroup mg = (MonitorGroup)group;
	    long   startDate     = mg.getStartDate();
	    long   period        = mg.getPeriod();
	    float  floatFraction = mg.getFloatFraction();
	    
	    // Special case: Not started monitoring.
	    if (startDate > te) {		
		logger.log(2, "Group "+group.getName()+" failed: MG_PRE_START");
		return null;
	    }

	    // Special case: Monitoring completed.
	    long endDate = mg.getEndDate();
	    if (endDate <= ts) {		
		logger.log(2, "Group "+group.getName()+" failed: MG_POST_END");
		return null;
	    }
	    
	    // first period before ts.
	    double fPeriod = (double)( ts - startDate ) / (double)period;
	    int i1 = (int)Math.floor(fPeriod);
	    
	    // last period after te.
	    fPeriod = (double)( te - startDate ) / (double)period;
	    int i2 = (int)Math.rint(fPeriod) + 1;

	    logger.log(2, "Group "+group.getName()+" Monitor: i1="+i1+" i2="+i2);

	    // Run through windows and see if overlapping
	    for (int i = i1; i < i2; i++) {

		// calculate the window
		long startFloat = startDate + 
		   (long) (((double)i - (double)floatFraction / 2.0)*period);

		long endFloat   = startDate + 
		    (long)(((double)i + (double)floatFraction / 2.0)*period);
		
		TimeWindow window = new TimeWindow(startFloat, endFloat);
		
		logger.log(2, "Group "+group.getName()+" Monitor: Checking "+i+"th window: "+window);

		if (startFloat > endDate) {
		    logger.log(2, "Group "+group.getName()+" Monitor: window past end-of-monitoring");
		} else if
		    (endFloat < startDate) {
		    logger.log(2, "Group "+group.getName()+" Monitor: window before start-of-monitoring");
		} else if 
		    ((startFloat <= te) && (endFloat >= ts)) {	   
		    // Check it wasnt done already inside the window     
		    //if (history.lastExecuted > 0 && 
		    if (history.lastExecuted < startFloat ) {		
			list.add(window);
			logger.log(2, "Group "+group.getName()+" Monitor: Window OK");
		    }
		}
	    }
	    
	} else if
	    (group instanceof FixedGroup) {
	    
	    // the only possible window is (fg-b, fg+b)
	    
	    FixedGroup fg = (FixedGroup)group;
	    
	    // Special case: Already done before ts	    
            if (history.lastExecuted > 0 && history.lastExecuted < ts) {
		logger.log(2, "Group "+group.getName()+" failed: FG_DONE");
                return null;
	    }	    
	    
	    // FG can be done within specified minutes of the fixed time.
	    long fixed = fg.getFixedTime();
	    // 	    if (  ((fixed - fixedGroupPreStartBufferTime) > te) ||
	    // 		  ((fixed + fixedGroupPostStartLapseTime) < ts) ) {
	    // 		logger.log(3, "Group "+group.getName()+" failed: FG_OUTSIDE_SLOP");
	    // 		return null;
	    // 	    }
            // TEMP Use fixed FGB time
	    if (  ((fixed - FGB) > te) ||
		  ((fixed + FGB) < ts) ) {
		logger.log(2, "Group "+group.getName()+" failed: FG_OUTSIDE_SLOP");
		return null;
	    }

	    list.add(new TimeWindow(fixed - FGB, fixed + FGB));
	    
	} else if 
	    (group instanceof RepeatableGroup) {

	    // depends on last exec from hist. Cannot predict future beyond one window
	    RepeatableGroup rg = (RepeatableGroup)group;

	    long startDate   = rg.getStartDate();
	    long endDate     = rg.getEndDate();
	    long minInterval = rg.getMinimumInterval();
	    int  maxRepeats  = rg.getMaximumRepeats();

	    // Special case: Not started monitoring.
	    if (startDate > te) {		
		logger.log(2, "Group "+group.getName()+" failed: RG_PRE_START");
		return null;
	    }

	    // Special case: Monitoring completed.
	    if (endDate < ts) {
		logger.log(2, "Group "+group.getName()+" failed: RG_PAST_END");
		return null;
	    }

	    // Special case: Too soon after last exec
	    //if((ts - history.lastExecuted) < minInterval) {
	    //logger.log(2, "Group "+group.getName()+" failed: REPEATABLE_TOO_SOON_AFTER_LAST_EXEC");
	    //return null;
	    //}

	    // Special case: Repeat count exceeded // we really need to be able to do hist.getLastExecBefore(time)
	    // as last exec could be after this sim time ? or can we purge this in the histModel.load(p2model, uptoTime) ?
	    if (maxRepeats < history.countExecutions) {
		logger.log(2, "Group "+group.getName()+" failed: REPEATABLE_TOO_MANY_REPEATS");
		return null;
	    }

	    // lets restrict the end time to the earlier of
	    // group.end, group.exp - we may well run over the TE value but thats just fine
	    //long intend = Math.min(grpExpDate, endDate);
	    //list.add(new TimeWindow(history.lastExecuted + minInterval, intend));
	    int i1 = (int)Math.floor((double)(ts - history.lastExecuted)/(double)minInterval);
	    int i2 = (int)Math.floor((double)(te - history.lastExecuted)/(double)minInterval);

	    for (int i = i1; i < i2; i++) {

		TimeWindow window = new TimeWindow(history.lastExecuted + i*minInterval,
						   history.lastExecuted + (i+1)*minInterval);
		list.add(window);
		logger.log(2, "Group "+group.getName()+" Adding "+i+"th window: "+window);
	    }

	} else if 
	    (group instanceof EphemerisGroup) {

	    // similar to mg but using phase - only if not already done by start
	    EphemerisGroup eg = (EphemerisGroup)group;
	    
	    long   startDate     = eg.getStart();
	    long   endDate       = eg.getEnd();
	    long   period        = eg.getPeriod();
	    double phase         = (double)eg.getPhase();
	    double slopFraction  = (double)eg.getSlopPhase();


	    // Special case: Already done before ts	    
            if (history.lastExecuted > 0 && history.lastExecuted < ts) {
		logger.log(2, "Group "+group.getName()+" failed: EG_DONE");
                return null;
	    }	    
	
	    // Special case: Not started monitoring.
	    if (startDate > te) {
		logger.log(2, "Group "+group.getName()+" failed: EG_PRE_START");
		return null;
	    }
	    // Special case: Monitoring completed.
	    if (endDate <= ts) {
		logger.log(2, "Group "+group.getName()+" failed: EG_POST_END");
		return null;
	    }

	    // first period before ts.
	    double fp = Math.floor((ts - startDate)/period);
	    	    
	    // calculate the window
	    long startWindow = startDate + (long)((fp + phase - slopFraction/2.0) * period);
	    long endWindow   = startDate + (long)((fp + phase + slopFraction/2.0) * period);
	    
	    if ( ((startWindow <= te) && (endWindow >= ts))) 
		list.add(new TimeWindow(startWindow, endWindow));	    
	    
	} else {
	    
	    // (fx.start, fx.end) but only if not already done by start.

	    long startFlex = group.getStartingDate();
	    long endFlex = grpExpDate;

	    // Special case: Already done before ts	    
            if (history.lastExecuted > 0 && history.lastExecuted < ts) {
		logger.log(2, "Group "+group.getName()+" failed: FLEXG_DONE");
                return null;
	    }	    
	    logger.log(2, "Group "+group.getName()+"BTCWC: OK Flexible group with SD="+(new Date(startFlex))+" ED="+(new Date(endFlex)));
	    if ((startFlex < te) &&
		(endFlex > ts))
		list.add(new TimeWindow(startFlex, endFlex));
	    
	}
	
	return list;
	
    }
    
    /** Returns how much time is left for a group to execute in the specified (pre-calculated) 
     * execution window starting from the specified time.
     * @param group The group we are interested in.
     * @param window The pre-calculated execution window to check.
     * @param t The time to start counting from.
     * @param env The group's environment.
     * @param hist The group's execution history (upto t ?).
     */
    public long calculateRemainingTime(Group group, 
				       TimeWindow window, 
				       long t, 
				       EnvironmentSnapshot env, 
				       ExecutionStatistics hist) {

	logger.method("calcRemainingTime(G,w,t,e,h)").
	    log(2, "Checking feasible windows for: Group: "+group.getName()+
		"between TS="+ScheduleSimulator.sdf.format(new Date(window.start))+
		"and TE="+ScheduleSimulator.sdf.format(new Date(window.end))+
		" start from time = "+ScheduleSimulator.sdf.format(new Date(t)));
	
	// An expired group  by t is out of luck...
	if (group.getExpiryDate() < t)
	    return 0L;

	long sum = 0L;
	long tt = Math.max(t,window.start);
	while (tt < window.end) {
	    if (execModel.canDo(group, tt, env, hist))
		sum += dt;
	    tt += dt;
	}
	
	return sum;
		
    }

    /** Returns how many nights the nominated group is executable on in the current window.*/
    public int countRemainingNights(Group group, 
				    TimeWindow window, 
				    long t, 
				    EnvironmentSnapshot env, 
				    ExecutionStatistics hist) {

	// first work out which day this time contains. Nights start at 1200 Local.
	logger.method("countRemainingNights(G,w,t,e,h)").
	       log(1, "Checking RN for group: "+group.getName());	
	logger.log(1, "Checking RN in window W="+window+" at "+ScheduleSimulator.sdf.format(new Date(t)));

	long sofn = calculatePreviousLocalNoon(t, site);

	// any days hereafter are relative to this

	// can we do the group on night 0 - this starts at t not 1200 ?
	int countExecutableNights = 0;
	if (executable(group, t, sofn + DAYMS, env,hist)) {
	    countExecutableNights++;
	    logger.method("countRemainingNights(G,w,t,e,h)").
		log(2, "Cando: Night: 0 from "+ ScheduleSimulator.sdf.format(new Date(t))+", "+
		    ScheduleSimulator.sdf.format(new Date(sofn+DAYMS)));
	}
	    
	int nn =(int)((double)(window.end - t)/(double)DAYMS) + 1;

	// for each following night...1200 to 1200
	logger.method("countRemainingNights(G,w,t,e,h)").
	    log(2, "Checking nights: 1 to "+nn); 
		
	for (int id = 1; id < nn; id++) {

	    long sd = sofn + (long)id*DAYMS;
	    long ed = sd  + DAYMS;
	
	    if (executable(group, sd, ed, env, hist)) {
		countExecutableNights++;
		logger.method("countRemainingNights(G,w,t,e,h)").
		    log(2, "Cando: Night: "+id+" from "+ScheduleSimulator.sdf.format(new Date(sd))+", "+
			ScheduleSimulator.sdf.format(new Date(ed)));
		
		
	    }
	}

	logger.method("countRemainingNights(G,w,t,e,h)").
	    log(1, "Group has "+countExecutableNights+" nights remaining in window");
	
	if (countExecutableNights == 0)
	    logger.method("countRemainingNights(G,w,t,e,h)").
		log(1, "WARNING: Group: "+group+" has RN 0 in window "+window+" at "+
		    ScheduleSimulator.sdf.format(new Date(t)));
	
	return countExecutableNights;

    }

    /** Work out when the last local noon occurred.*/
    private long calculatePreviousLocalNoon(long time, Site site) {
	Calendar cal = Calendar.getInstance();
	cal.setTime(new Date(time));
	cal.set(Calendar.MINUTE, 0);
	cal.set(Calendar.SECOND, 0);
	long nts = cal.getTime().getTime();

	int hh = cal.get(Calendar.HOUR_OF_DAY);
	hh = hh + (int) Math.floor(Math.toDegrees(site.getLongitude()) / 15.0);

	if (hh < 0)
	    hh += 24;
	if (hh > 24)
	    hh -= 24;

	// compute last valid local noon of first active night
	long sofn = 0L;
	if (hh <= 12)
	    sofn = nts - 3600000L * (12 + hh);
	else
	    sofn = nts - 3600000L * (hh - 12);

	return sofn;
    }


    /** Is the group executable under env given hist.*/
    private boolean executable(Group group, 
			       long t1, 
			       long t2, 
			       EnvironmentSnapshot env,
			       ExecutionStatistics hist) {

	// run over the night check for xm.canDo();

	int nsamp = (int)((t2 - t1)/SAMP_INT);
	logger.method("executable").log(3, "Testing with "+nsamp+" samples");

	long t = t1;
	while (t < t2) {
	    // fallout first valid time
	    if (execModel.canDo(group, t, env, hist))
		return true;
	    t += SAMP_INT;
	}

	return false;
    }


}
